package game.game.editors;

import com.crunch.core.Actor;
import com.crunch.core.Input;
import com.crunch.core.Renderable;
import com.crunch.graphics.gpu.GpuBlendState;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.graphics.gpu.GpuTexture;
import com.crunch.math.Matrix33af;
import com.crunch.math.Matrix44f;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;
import game.game.RoomDefinition;
import game.game.RoomState;
import scenery.TileLayer;
import scenery.TileLayerInstance;
import util.Camera;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Allows editing of tile layers.
 */
public class TileEditor extends Actor implements Renderable {
	public TileEditor() {
		super(-50);
	}

	@Override
	public void onCreate() {
		getEngine().window.renderer.register(this);
		commandQueue = new ConcurrentLinkedQueue<Runnable>();

		// create list of textures
		Map<String, GpuTexture> textures = ((RoomState) getGameState()).getCoreState().resourceManager.getTextures();
		textureNames = new Object[textures.size()];
		int i = 0;
		for (String s : textures.keySet()) {
			textureNames[i] = s;
			++i;
		}

		editor = new TileEditorFrame();

		eventListener = new Input.EventListener() {
			@Override
			public void processEvent(AWTEvent e, int eventType) {
				if (eventType == Input.MOUSE_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (ev.getButton() == MouseEvent.BUTTON2) {
						// camera drag
						if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
							RoomState roomState = (RoomState) getGameState();
							int scale = roomState.getCoreState().getWindowScale();
							draggingCamera = true;
							dragX = ev.getX() / scale;
							dragY = ev.getY() / scale;
						} else if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
							draggingCamera = false;
						}
					} else if (ev.getButton() == MouseEvent.BUTTON1 || ev.getButton() == MouseEvent.BUTTON3) {
						RoomState roomState = (RoomState) getGameState();
						int scale = roomState.getCoreState().getWindowScale();
						if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
							mousePressed(ev.getX() / scale, ev.getY() / scale, ev.getButton());
						} else if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
							mouseReleased(ev.getX() / scale, ev.getY() / scale, ev.getButton());
						}
					}
				} else if (eventType == Input.MOUSE_MOTION_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (draggingCamera) {
						RoomState roomState = (RoomState) getGameState();
						int scale = roomState.getCoreState().getWindowScale();
						int newDragX = ev.getX() / scale;
						int newDragY = ev.getY() / scale;
						int dx = newDragX - dragX;
						int dy = dragY - newDragY; // flip y

						if (!showTileset) {
							// move camera
							Camera camera = roomState.getCamera();
							int newX = (int) camera.getX() - dx;
							int newY = (int) camera.getY() - dy;

							// clamp to room size
							int xMin = newX - camera.getViewportWidth()/2;
							int yMin = newY - camera.getViewportHeight()/2;
							int xMax = xMin + camera.getViewportWidth();
							int yMax = yMin + camera.getViewportHeight();
							if (xMax > roomState.getRoomWidth()) {
								int diff = xMax - roomState.getRoomWidth();
								newX -= diff;
								xMin -= diff;
							}
							if (yMax > roomState.getRoomHeight()) {
								int diff = yMax - roomState.getRoomHeight();
								newY -= diff;
								yMin -= diff;
							}
							if (xMin < 0) {
								int diff = -xMin;
								newX += diff;
							}
							if (yMin < 0) {
								int diff = -yMin;
								newY += diff;
							}

							camera.set((float) newX, (float) newY, camera.getZoom());
						} else {
							if (activeTileLayer != null && activeTileLayer.getTexture() != null) {
								Camera guiCamera = roomState.getGuiCamera();
								// move tileset
								tilesetDragX += dx;
								tilesetDragY += dy;
								// clamp tileset drag
								// tilesetDrag is offset of tileset's TOP LEFT corner from viewport's TOP LEFT corner
								int right = tilesetDragX + activeTileLayer.getTexture().getWidth();
								if (right < guiCamera.getViewportWidth()) {
									tilesetDragX += guiCamera.getViewportWidth() - right;
								}
								int bottom = guiCamera.getViewportHeight() - activeTileLayer.getTexture().getHeight() + tilesetDragY;
								if (bottom > 0) {
									tilesetDragY -= bottom;
								}
								if (tilesetDragX > 0) {
									tilesetDragX = 0;
								}
								if (tilesetDragY < 0) {
									tilesetDragY = 0;
								}
							}
						}
						dragX = newDragX;
						dragY = newDragY;
					}
				} else if (eventType == Input.KEY_EVENT) {
					KeyEvent ev = (KeyEvent) e;
					if (ev.getKeyCode() == KeyEvent.VK_TAB && ev.getID() == KeyEvent.KEY_PRESSED) {
						showTileset = !showTileset;
					}
				}
			}
		};

		eventListener.eventMask = Input.MOUSE_EVENT | Input.MOUSE_MOTION_EVENT | Input.KEY_EVENT;
		getEngine().input.addEventListener(eventListener);

		tilesAdded = new HashSet<Point>();
		tileLayersVisible = new HashMap<TileLayerInstance, Boolean>();
	}

	// initializes editor with existing tile layers
	public void init(RoomDefinition roomDefinition) {
		this.roomDefinition = roomDefinition;
		RoomState roomState = (RoomState) getGameState();
		for (TileLayerInstance instance : roomState.getTileLayers()) {
			tileLayersVisible.put(instance, true);
		}
		editor.init();
	}

	@Override
	public void onUpdate() {
		// run all commands
		Runnable cmd;
		while ((cmd = commandQueue.poll()) != null) {
			cmd.run();
		}

		RoomState roomState = (RoomState) getGameState();

		if (activeTileLayer == null || activeTileLayer.getTexture() == null) {
			showTileset = false;
			addingTiles = false;
		}
		if (!showTileset) {
			pickingTile = false;
		}

		if (pickingTile) {
			int vh = roomState.getGuiCamera().getViewportHeight();
			int tw = activeTileLayer.getTexture().getWidth();
			int th = activeTileLayer.getTexture().getHeight();
			int scale = roomState.getCoreState().getWindowScale();
			Point mousePosition = getEngine().input.getMousePosition();
			mousePosition.x /= scale;
			mousePosition.y /= scale;

			mousePosition.y = vh - mousePosition.y - 1;

			// convert to tile coords

			// first, subtract tileset origin
			int ax = pickXStart - tilesetDragX;
			int ay = pickYStart - (vh - th) - tilesetDragY;
			int bx = mousePosition.x - tilesetDragX;
			int by = mousePosition.y - (vh - th) - tilesetDragY;

			ax = Math.max(0, Math.min(tw-1, ax));
			ay = Math.max(0, Math.min(th-1, ay));
			bx = Math.max(0, Math.min(tw-1, bx));
			by = Math.max(0, Math.min(th-1, by));

			int offX = imageGridOffsetX % imageGridWidth;
			int offY = imageGridOffsetY % imageGridHeight;
			ax = (ax < offX) ? 0 : ((ax - offX) / imageGridWidth) * imageGridWidth + offX;
			ay = (ay < offY) ? 0 : ((ay - offY) / imageGridHeight) * imageGridHeight + offY;
			bx = (bx < offX) ? 0 : ((bx - offX) / imageGridWidth) * imageGridWidth + offX;
			by = (by < offY) ? 0 : ((by - offY) / imageGridHeight) * imageGridHeight + offY;

			if (bx < ax) {
				// swap
				int temp = ax;
				ax = bx;
				bx = temp;
			}
			if (by < ay) {
				int temp = ay;
				ay = by;
				by = temp;
			}

			tileX = ax;
			tileY = ay;
			int tileX1 = Math.min(bx + imageGridWidth, tw);
			int tileY1 = Math.min(by + imageGridHeight, th);
			tileW = tileX1 - tileX;
			tileH = tileY1 - tileY;
		} else if (activeTileLayer != null) {
			int vh = roomState.getCamera().getViewportHeight();
			int scale = roomState.getCoreState().getWindowScale();
			Point mousePosition = getEngine().input.getMousePosition();
			mousePosition.x /= scale;
			mousePosition.y /= scale;

			mousePosition.y = vh - mousePosition.y - 1;

			// convert to room coords
			int cornerX = (int) roomState.getCamera().getX() - roomState.getCamera().getViewportWidth()/2;
			int cornerY = (int) roomState.getCamera().getY() - roomState.getCamera().getViewportHeight()/2;
			int offsetX = Math.round((float) cornerX * activeTileLayer.getTileLayer().getParallaxX());
			int offsetY = Math.round((float) cornerY * activeTileLayer.getTileLayer().getParallaxY());
			int x = cornerX + mousePosition.x - offsetX;
			int y = cornerY + mousePosition.y - offsetY;

			int offX = gridOffsetX % gridWidth;
			int offY = gridOffsetY % gridHeight;
			int alignedX = (x < offX) ? 0 : ((x - offX) / gridWidth) * gridWidth + offX;
			int alignedY = (y < offY) ? 0 : ((y - offY) / gridHeight) * gridHeight + offY;

			tileRoomX = alignedX;
			tileRoomY = alignedY;

			if (addingTiles && tileW > 0 && tileH > 0) {
				Point tilePoint = new Point(tileRoomX, tileRoomY);
				if (!tilesAdded.contains(tilePoint)) {
					// add to the map
					tilesAdded.add(tilePoint);
					TileLayer.Tile tile = new TileLayer.Tile();
					tile.setX(tileRoomX);
					tile.setY(tileRoomY);
					tile.setImageX((short) tileX);
					tile.setImageY((short) tileY);
					tile.setWidth((short) tileW);
					tile.setHeight((short) tileH);
					tile.setFrames((short) tileFrames);
					tile.setFrameTime((short) tileFrameTime);
					if (randomizeTileTimeOffset) {
						int timeOffset = (alignTileTimeOffsetWithFrameTime) ?
								random.nextInt(tileFrames) * tileFrameTime :
								random.nextInt(tileFrames * tileFrameTime);
						tile.setTimeOffset((short) timeOffset);
					} else {
						tile.setTimeOffset((short) tileTimeOffset);
					}
					tile.setFrameOffsetX((short) tileFrameOffsetX);
					tile.setFrameOffsetY((short) tileFrameOffsetY);
					activeTileLayer.getTileLayer().addTile(tile);
					activeTileLayer.regionUpdated(tile.getX(), tile.getY(), tile.getWidth(), tile.getHeight());
				}
			} else if (removingTiles) {
				// only remove tiles when the mouse moves
				if (removeX != x || removeY != y) {
					int[] tiles = activeTileLayer.getTileLayer().getTilesAtPosition(x, y);
					if (tiles.length > 0) {
						// remove only the top tile each time
						TileLayer.Tile tile = activeTileLayer.getTileLayer().getTile(tiles[tiles.length-1]);
						activeTileLayer.getTileLayer().removeTile(tiles[tiles.length-1]);
						activeTileLayer.regionUpdated(tile.getX(), tile.getY(), tile.getWidth(), tile.getHeight());
						removeX = x;
						removeY = y;
					}
				}
			}
		}
	}

	@Override
	public void onDestroy() {
		hideUnselectedLayers = false;
		updateVisibility();

		getEngine().window.renderer.unregister(this);
		editor.dispose();

		getEngine().input.removeEventListener(eventListener);
	}

	private RoomDefinition roomDefinition;
	private boolean draggingCamera = false;
	private int dragX, dragY;

	private TileLayerInstance activeTileLayer;
	private HashMap<TileLayerInstance, Boolean> tileLayersVisible; // used to keep track of tile layer visibility

	private boolean showTileset = false;
	private Input.EventListener eventListener;
	private boolean pickingTile;
	private int pickXStart, pickYStart;
	private int tilesetDragX, tilesetDragY;

	private boolean showGrid = false;
	private int gridWidth = 16;
	private int gridHeight = 16;
	private int gridOffsetX = 0;
	private int gridOffsetY = 0;

	private boolean showImageGrid = false;
	private int imageGridWidth = 16;
	private int imageGridHeight = 16;
	private int imageGridOffsetX = 0;
	private int imageGridOffsetY = 0;

	private boolean hideUnselectedLayers = false;

	// map of tiles added in this click, so that duplicates aren't placed
	private HashSet<Point> tilesAdded;
	private boolean addingTiles;
	private int tileX, tileY, tileW, tileH;
	private int tileRoomX, tileRoomY;
	private int tileFrames = 1;
	private int tileFrameTime = 1;
	private int tileTimeOffset = 0;
	private boolean randomizeTileTimeOffset = false;
	private boolean alignTileTimeOffsetWithFrameTime = true;
	private Random random = new Random();
	private int tileFrameOffsetX = 0;
	private int tileFrameOffsetY = 0;

	private boolean removingTiles;
	private int removeX, removeY;

	private Object[] textureNames;

	private void updateVisibility() {
		RoomState roomState = (RoomState) getGameState();
		for (Map.Entry<TileLayerInstance, Boolean> e : tileLayersVisible.entrySet()) {
			boolean shouldBeVisible = !hideUnselectedLayers || (activeTileLayer == e.getKey());
			if (shouldBeVisible && !e.getValue()) {
				// show
				roomState.addTileLayer(e.getKey());
			} else if (!shouldBeVisible && e.getValue()) {
				roomState.removeTileLayer(e.getKey());
			}
			e.setValue(shouldBeVisible);
		}
	}

	private void mousePressed(int x, int y, int button) {
		if (button == MouseEvent.BUTTON1 && !removingTiles) {
			if (showTileset) {
				pickingTile = true;
				RoomState roomState = (RoomState) getGameState();
				y = roomState.getGuiCamera().getViewportHeight() - y - 1;
				pickXStart = x;
				pickYStart = y;
			} else {
				addingTiles = true;
				tilesAdded.clear();
			}
		} else if (button == MouseEvent.BUTTON3 && !addingTiles) {
			if (!showTileset) {
				removingTiles = true;
				removeX = Integer.MIN_VALUE;
				removeY = Integer.MIN_VALUE;
			}
		}
	}

	private void mouseReleased(int x, int y, int button) {
		if (button == MouseEvent.BUTTON1) {
			pickingTile = false;
			addingTiles = false;
		} else if (button == MouseEvent.BUTTON3) {
			removingTiles = false;
		}
	}

	@Override
	public void render(GpuState gpuState) {
		GpuBlendState blendState = gpuState.createBlendState();

		RoomState roomState = (RoomState) getGameState();
		Camera guiCamera = roomState.getGuiCamera();
		Camera camera = roomState.getCamera();
		Matrix33af modelMatrix = new Matrix33af();

		// draw the screen grid
		if (activeTileLayer != null && showGrid && gridWidth > 3 && gridHeight > 3) {
			// determine offset using parallax
			int cornerX = (int) camera.getX() - camera.getViewportWidth()/2;
			int cornerY = (int) camera.getY() - camera.getViewportHeight()/2;
			int offsetX = Math.round((float) cornerX * activeTileLayer.getTileLayer().getParallaxX()) + gridOffsetX;
			int offsetY = Math.round((float) cornerY * activeTileLayer.getTileLayer().getParallaxY()) + gridOffsetY;

			offsetX %= gridWidth;
			offsetY %= gridHeight;

			Vector2f[] bounds = roomState.getCamera().getViewBounds();
			int xMin = (int) Math.floor(bounds[0].x());
			int yMin = (int) Math.floor(bounds[0].y());
			int xMax = (int) Math.ceil(bounds[1].x());
			int yMax = (int) Math.ceil(bounds[1].y());

			Vector2f minBound = new Vector2f();
			Vector2f maxBound = new Vector2f();
			Vector4f gridColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
			// draw grid with "inverted" colors
			blendState.setBlendFunction(GpuBlendState.BlendMode.INVERSE);
			blendState.bind();

			// draw each grid point
			// determine the first point to draw
			int startX = (xMin / gridWidth) * gridWidth + offsetX;
			int startY = (yMin / gridHeight) * gridHeight + offsetY;
			for (int y = startY; y <= yMax; y += gridHeight) {
				for (int x = startX; x <= xMax; x += gridWidth) {
					minBound.set((float) x, (float) y);
					maxBound.set(minBound.x() + 1.0f, minBound.y() + 1.0f);
					roomState.getSceneryRenderer().drawFilledRect(gpuState,
							camera.getViewProjectionMatrix(),
							minBound,
							maxBound,
							gridColor);
				}
			}
		}

		if (!showTileset && tileW > 0 && tileH > 0) {
			int cornerX = (int) roomState.getCamera().getX() - roomState.getCamera().getViewportWidth()/2;
			int cornerY = (int) roomState.getCamera().getY() - roomState.getCamera().getViewportHeight()/2;
			int offsetX = Math.round((float) cornerX * activeTileLayer.getTileLayer().getParallaxX());
			int offsetY = Math.round((float) cornerY * activeTileLayer.getTileLayer().getParallaxY());

			// draw the tile location on the grid
			// draw grid with "inverted" colors
			blendState.setBlendFunction(GpuBlendState.BlendMode.INVERSE);
			blendState.bind();
			Vector2f minBound = new Vector2f((float) (tileRoomX + offsetX), (float) (tileRoomY + offsetY));
			Vector2f maxBound = new Vector2f(minBound.x() + (float) tileW, minBound.y() + (float) tileH);
			Vector4f color = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
			roomState.getSceneryRenderer().drawRect(gpuState,
					camera.getViewProjectionMatrix(),
					minBound,
					maxBound,
					color, 1.0f);
		}

		if (activeTileLayer != null && activeTileLayer.getTexture() != null && showTileset) {
			blendState.setBlendFunction(GpuBlendState.BlendMode.ALPHA);
			blendState.bind();

			modelMatrix.setIdentity();
			int w = activeTileLayer.getTexture().getWidth();
			int h = activeTileLayer.getTexture().getHeight();
			modelMatrix.translateAssign(new Vector2f(
					(float) tilesetDragX,
					(float) (guiCamera.getViewportHeight() - h + tilesetDragY)));
			Matrix44f mvpMatrix = guiCamera.getViewProjectionMatrix().multiply(modelMatrix.getGraphicsMatrix());
			roomState.getSceneryRenderer().drawFilledRect(gpuState,
					mvpMatrix,
					new Vector2f(0.0f, 0.0f),
					new Vector2f((float) w, (float) h),
					new Vector4f(0.0f, 0.0f, 0.0f, 0.5f));
			roomState.getSceneryRenderer().drawSprite(gpuState,
					mvpMatrix,
					activeTileLayer.getTexture(),
					activeTileLayer.getTileLayer().getColor(),
					new Vector2f(0.0f, 0.0f), new Vector2f(w, h), new Vector2f(0.0f, 0.0f));

			// draw grid, but only if it's not too dense
			if (showImageGrid && imageGridWidth > 3 && imageGridHeight > 3) {
				Vector2f minBound = new Vector2f();
				Vector2f maxBound = new Vector2f();
				Vector4f gridColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
				// draw grid with "inverted" colors
				blendState.setBlendFunction(GpuBlendState.BlendMode.INVERSE);
				blendState.bind();
				for (int y = imageGridOffsetY % imageGridHeight; y < activeTileLayer.getTexture().getHeight(); y += imageGridHeight) {
					for (int x = imageGridOffsetX % imageGridWidth; x < activeTileLayer.getTexture().getWidth(); x += imageGridWidth) {
						minBound.set((float) (x + tilesetDragX), (float) (y + guiCamera.getViewportHeight() - h + tilesetDragY));
						maxBound.set(minBound.x() + 1.0f, minBound.y() + 1.0f);
						roomState.getSceneryRenderer().drawFilledRect(gpuState,
								guiCamera.getViewProjectionMatrix(),
								minBound,
								maxBound,
								gridColor);
					}
				}
			}

			// draw selected tile
			if (tileW > 0 && tileH > 0) {
				// draw grid with "inverted" colors
				blendState.setBlendFunction(GpuBlendState.BlendMode.INVERSE);
				blendState.bind();
				int bx = tileX + tilesetDragX;
				int by = tileY + guiCamera.getViewportHeight() - h + tilesetDragY;
				Vector2f minBound = new Vector2f();
				Vector2f maxBound = new Vector2f();
				Vector4f color = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);

				// if offsets are negative, make them positive
				int offX = tileFrameOffsetX;
				int offY = tileFrameOffsetY;

				if ((offX == 0 && offY == 0) || tileFrames == 1) {
					// draw single rectangle
					minBound.set((float) bx, (float) by);
					maxBound.set(minBound.x() + (float) tileW, minBound.y() + (float) tileH);
					roomState.getSceneryRenderer().drawRect(gpuState,
							guiCamera.getViewProjectionMatrix(),
							minBound,
							maxBound,
							color, 1.0f);
				} else if (Math.abs(offX) >= tileW || Math.abs(offY) >= tileH) {
					// draw series of rectangles
					for (int i = 0; i < tileFrames; ++i) {
						minBound.set((float) bx, (float) by);
						maxBound.set(minBound.x() + (float) tileW, minBound.y() + (float) tileH);
						roomState.getSceneryRenderer().drawRect(gpuState,
								guiCamera.getViewProjectionMatrix(),
								minBound,
								maxBound,
								color, 1.0f);
						bx += offX;
						by += offY;
					}
				} else if (offX == 0 || offY == 0) {
					if (offX < 0) {
						bx += offX * (tileFrames-1);
						offX = -offX;
					}
					if (offY < 0) {
						by += offY * (tileFrames-1);
						offY = -offY;
					}

					// draw bounding box of entire tile and then draw borders inside it
					minBound.set((float) bx, (float) by);
					maxBound.set((float) (bx + tileW + offX * (tileFrames-1)),
							(float) (by + tileH + offY * (tileFrames-1)));
					roomState.getSceneryRenderer().drawRect(gpuState,
							guiCamera.getViewProjectionMatrix(),
							minBound,
							maxBound,
							color, 1.0f);
					// go through each frame and draw min/max bounds, ordered by coordinates so we avoid duplicates
					int nextMinBound = 1; // skip first
					int nextMaxBound = 0;
					int start = (offY == 0) ? bx : by;
					int sizeA = (offY == 0) ? tileW : tileH;
					int sizeB = (offY == 0) ? tileH : tileW;
					int off = (offY == 0) ? offX : offY;
					if (sizeB > 2) {
						while (nextMinBound < tileFrames || nextMaxBound < tileFrames-1) {
							boolean minValid = nextMinBound < tileFrames;
							boolean maxValid = nextMaxBound < tileFrames-1;
							int minPos = start + nextMinBound * off;
							int maxPos = start + sizeA + nextMaxBound * off - 1;
							int drawPos;
							if (minValid && (minPos < maxPos || !maxValid)) {
								drawPos = minPos;
								++nextMinBound;
							} else if (maxValid && (maxPos < minPos || !minValid)) {
								drawPos = maxPos;
								++nextMaxBound;
							} else {
								drawPos = minPos;
								++nextMinBound;
								++nextMaxBound;
							}
							if (offY == 0) {
								// draw on x
								minBound.set((float) drawPos, (float) (by+1));
								maxBound.set((float) (drawPos+1), (float) (by + tileH - 1));
							} else {
								// draw on y
								minBound.set((float) (bx+1), (float) drawPos);
								maxBound.set((float) (bx + tileW - 1), (float) (drawPos+1));
							}
							roomState.getSceneryRenderer().drawFilledRect(gpuState, guiCamera.getViewProjectionMatrix(), minBound, maxBound, color);
						}
					}
				} else {
					// draw series of rectangles
					// redraw overlapping points
					for (int i = 0; i < tileFrames; ++i) {
						minBound.set((float) bx, (float) by);
						maxBound.set(minBound.x() + (float) tileW, minBound.y() + (float) tileH);
						roomState.getSceneryRenderer().drawRect(gpuState,
								guiCamera.getViewProjectionMatrix(),
								minBound,
								maxBound,
								color, 1.0f);
						// intersect with previous rectangles
						for (int t = i-1; t >= 0; --t) {
							int i1 = i - t;
							// intersect the two boxes - max of mins, min of maxes
							int xMin = Math.max(bx, bx - i1*offX);
							int yMin = Math.max(by, by - i1*offY);
							int xMax = Math.min(bx, bx - i1*offX) + tileW;
							int yMax = Math.min(by, by - i1*offY) + tileH;
							if (xMax - xMin > 1 && yMax - yMin > 1) {
								// determine which 2 points to redraw based on offsets
								int isectAX = xMin;
								int isectAY = (offX > 0 == offY > 0) ? yMax - 1 : yMin;
								int isectBX = xMax - 1;
								int isectBY = (offX > 0 == offY > 0) ? yMin : yMax - 1;
								minBound.set((float) isectAX, (float) isectAY);
								maxBound.set((float) (isectAX+1), (float) (isectAY+1));
								roomState.getSceneryRenderer().drawFilledRect(gpuState, guiCamera.getViewProjectionMatrix(), minBound, maxBound, color);
								minBound.set((float) isectBX, (float) isectBY);
								maxBound.set((float) (isectBX+1), (float) (isectBY+1));
								roomState.getSceneryRenderer().drawFilledRect(gpuState, guiCamera.getViewProjectionMatrix(), minBound, maxBound, color);
							} else if (xMax - xMin == 1 || yMax - yMin == 1) {
								minBound.set((float) xMin, (float) yMin);
								maxBound.set((float) xMax, (float) yMax);
								roomState.getSceneryRenderer().drawFilledRect(gpuState, guiCamera.getViewProjectionMatrix(), minBound, maxBound, color);
							} else {
								break; // no intersection, so there will be none with the next rectangle either
							}
						}
						bx += offX;
						by += offY;
					}
				}
			}
		}
	}

	@Override
	public int getDepth() {
		return Integer.MAX_VALUE - 10;
	}

	Queue<Runnable> commandQueue;

	TileEditorFrame editor;
	private class TileEditorFrame extends JFrame {
		private class Node {
			public TileLayerInstance layer;
			public String text;
			public String toString() {
				return text;
			}
		}

		private JCheckBox showGridCheckbox;
		private JSpinner gridWidthSpinner;
		private JSpinner gridHeightSpinner;
		private JSpinner gridXSpinner;
		private JSpinner gridYSpinner;

		private JCheckBox showImageGridCheckbox;
		private JSpinner imageGridWidthSpinner;
		private JSpinner imageGridHeightSpinner;
		private JSpinner imageGridXSpinner;
		private JSpinner imageGridYSpinner;

		private JCheckBox hideUnselectedLayersCheckbox;

		private JButton addButton;
		private JButton removeButton;
		private JList<Node> list;
		private DefaultListModel<Node> model;
		private JButton setTextureButton;
		private JSpinner depthSpinner;
		private JSpinner parallaxXSpinner;
		private JSpinner parallaxYSpinner;
		private JPanel colorChooser;
		private JSpinner alphaSpinner;

		private JSpinner framesSpinner;
		private JSpinner frameTimeSpinner;
		private JSpinner timeOffsetSpinner;
		private JSpinner frameOffsetXSpinner;
		private JSpinner frameOffsetYSpinner;
		private JCheckBox randomizeTimeOffsetCheckbox;
		private JCheckBox alignTimeOffsetWithFrameTimeCheckbox;

		private boolean performCallbacks;

		public TileEditorFrame() {
			setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);

			setTitle("Tile editor");

			JPanel contentPanel = new JPanel();
			add(contentPanel);

			GroupLayout layout = new GroupLayout(contentPanel);
			contentPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			showGridCheckbox = new JCheckBox("Show grid");
			showGridCheckbox.setSelected(showGrid);
			JLabel gridSizeLabel = new JLabel("Grid size:");
			gridWidthSpinner = new JSpinner(new SpinnerNumberModel(gridWidth, 1, 512, 1));
			gridHeightSpinner = new JSpinner(new SpinnerNumberModel(gridHeight, 1, 512, 1));
			JLabel gridOffsetLabel = new JLabel("Grid offset:");
			gridXSpinner = new JSpinner(new SpinnerNumberModel(gridOffsetX, 0, 512, 1));
			gridYSpinner = new JSpinner(new SpinnerNumberModel(gridOffsetY, 0, 512, 1));

			showImageGridCheckbox = new JCheckBox("Show image grid");
			showImageGridCheckbox.setSelected(showImageGrid);
			JLabel imageGridSizeLabel = new JLabel("Image grid size:");
			imageGridWidthSpinner = new JSpinner(new SpinnerNumberModel(imageGridWidth, 1, 512, 1));
			imageGridHeightSpinner = new JSpinner(new SpinnerNumberModel(imageGridHeight, 1, 512, 1));
			JLabel imageGridOffsetLabel = new JLabel("Image grid offset:");
			imageGridXSpinner = new JSpinner(new SpinnerNumberModel(imageGridOffsetX, 0, 512, 1));
			imageGridYSpinner = new JSpinner(new SpinnerNumberModel(imageGridOffsetY, 0, 512, 1));

			hideUnselectedLayersCheckbox = new JCheckBox("Hide unselected layers");
			hideUnselectedLayersCheckbox.setSelected(hideUnselectedLayers);

			JSeparator separatorA = new JSeparator();

			addButton = new JButton("Add");
			removeButton = new JButton("Remove");

			model = new DefaultListModel<Node>();
			list = new JList<Node>(model);
			JScrollPane listScroll = new JScrollPane(list);

			setTextureButton = new JButton("Set texture");

			JLabel depthLabel = new JLabel("Depth:");
			depthSpinner = new JSpinner(new SpinnerNumberModel(0, Integer.MIN_VALUE, Integer.MAX_VALUE, 1));
			depthSpinner.setMinimumSize(new Dimension(32, depthSpinner.getMinimumSize().height));

			JLabel parallaxLabel = new JLabel("Parallax:");
			parallaxXSpinner = new JSpinner(new SpinnerNumberModel(
					new Float(0.0f), new Float(-Float.MAX_VALUE), new Float(1.0f), new Float(0.25f)));
			parallaxXSpinner.setMinimumSize(new Dimension(32, parallaxXSpinner.getMinimumSize().height));
			parallaxYSpinner = new JSpinner(new SpinnerNumberModel(
					new Float(0.0f), new Float(-Float.MAX_VALUE), new Float(1.0f), new Float(0.25f)));
			parallaxYSpinner.setMinimumSize(new Dimension(32, parallaxYSpinner.getMinimumSize().height));

			JLabel colorLabel = new JLabel("Color:");
			colorChooser = new JPanel();
			colorChooser.setPreferredSize(new Dimension(colorChooser.getPreferredSize().width, 16));
			colorChooser.setMinimumSize(new Dimension(colorChooser.getMinimumSize().width, 16));
			colorChooser.setMaximumSize(new Dimension(colorChooser.getMaximumSize().width, 16));
			colorChooser.setBorder(BorderFactory.createLineBorder(Color.BLACK));

			JLabel alphaLabel = new JLabel("A:");
			alphaSpinner = new JSpinner(new SpinnerNumberModel(
					new Float(1.0f), new Float(0.0f), new Float(1.0f), new Float(0.125f)));
			// keep alpha spinner small
			alphaSpinner.setMaximumSize(new Dimension(48, alphaSpinner.getMinimumSize().height));

			JSeparator separatorB = new JSeparator();

			JLabel framesLabel = new JLabel("Frames:");
			framesSpinner = new JSpinner(new SpinnerNumberModel(tileFrames, 1, Short.MAX_VALUE, 1));

			JLabel frameTimeLabel = new JLabel("Frame time:");
			frameTimeSpinner = new JSpinner(new SpinnerNumberModel(tileFrameTime, 1, Short.MAX_VALUE, 1));

			JLabel timeOffsetLabel = new JLabel("Time offset:");
			timeOffsetSpinner = new JSpinner(new SpinnerNumberModel(tileTimeOffset, 0, Short.MAX_VALUE, 1));

			JLabel frameOffsetLabel = new JLabel("Frame offset:");
			frameOffsetXSpinner = new JSpinner(new SpinnerNumberModel(tileFrameOffsetX, Short.MIN_VALUE, Short.MAX_VALUE, 1));
			frameOffsetXSpinner.setMinimumSize(new Dimension(32, frameOffsetXSpinner.getMinimumSize().height));
			frameOffsetYSpinner = new JSpinner(new SpinnerNumberModel(tileFrameOffsetY, Short.MIN_VALUE, Short.MAX_VALUE, 1));
			frameOffsetYSpinner.setMinimumSize(new Dimension(32, frameOffsetYSpinner.getMinimumSize().height));

			randomizeTimeOffsetCheckbox = new JCheckBox("Randomize time offset");
			randomizeTimeOffsetCheckbox.setSelected(randomizeTileTimeOffset);

			alignTimeOffsetWithFrameTimeCheckbox = new JCheckBox("Align time offset");
			alignTimeOffsetWithFrameTimeCheckbox.setSelected(alignTileTimeOffsetWithFrameTime);

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
									.addComponent(gridSizeLabel)
									.addComponent(gridOffsetLabel)
									.addComponent(imageGridSizeLabel)
									.addComponent(imageGridOffsetLabel))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
									.addComponent(showGridCheckbox)
									.addGroup(layout.createSequentialGroup()
											.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
													.addComponent(gridWidthSpinner)
													.addComponent(gridXSpinner))
											.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
													.addComponent(gridHeightSpinner)
													.addComponent(gridYSpinner)))
									.addComponent(showImageGridCheckbox)
									.addGroup(layout.createSequentialGroup()
											.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
													.addComponent(imageGridWidthSpinner)
													.addComponent(imageGridXSpinner))
											.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
													.addComponent(imageGridHeightSpinner)
													.addComponent(imageGridYSpinner)))
									.addComponent(hideUnselectedLayersCheckbox)))
					.addComponent(separatorA)
					.addGroup(layout.createSequentialGroup()
							.addComponent(addButton)
							.addComponent(removeButton))
					.addComponent(listScroll)
					.addGroup(layout.createSequentialGroup()
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
									.addComponent(depthLabel)
									.addComponent(parallaxLabel)
									.addComponent(colorLabel))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
									.addComponent(setTextureButton)
									.addComponent(depthSpinner)
									.addGroup(layout.createSequentialGroup()
											.addComponent(parallaxXSpinner)
											.addComponent(parallaxYSpinner))
									.addGroup(layout.createSequentialGroup()
											.addComponent(colorChooser)
											.addComponent(alphaLabel)
											.addComponent(alphaSpinner))))
					.addComponent(separatorB)
					.addGroup(layout.createSequentialGroup()
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
									.addComponent(framesLabel)
									.addComponent(frameTimeLabel)
									.addComponent(timeOffsetLabel)
									.addComponent(frameOffsetLabel))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
									.addComponent(framesSpinner)
									.addComponent(frameTimeSpinner)
									.addComponent(timeOffsetSpinner)
									.addGroup(layout.createSequentialGroup()
											.addComponent(frameOffsetXSpinner)
											.addComponent(frameOffsetYSpinner))
									.addComponent(randomizeTimeOffsetCheckbox)
									.addComponent(alignTimeOffsetWithFrameTimeCheckbox))));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addComponent(showGridCheckbox)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(gridSizeLabel)
							.addComponent(gridWidthSpinner)
							.addComponent(gridHeightSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(gridOffsetLabel)
							.addComponent(gridXSpinner)
							.addComponent(gridYSpinner))
					.addComponent(showImageGridCheckbox)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(imageGridSizeLabel)
							.addComponent(imageGridWidthSpinner)
							.addComponent(imageGridHeightSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(imageGridOffsetLabel)
							.addComponent(imageGridXSpinner)
							.addComponent(imageGridYSpinner))
					.addComponent(hideUnselectedLayersCheckbox)
					.addComponent(separatorA, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE,
							GroupLayout.PREFERRED_SIZE)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addButton)
							.addComponent(removeButton))
					.addComponent(listScroll)
					.addComponent(setTextureButton)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(parallaxLabel)
							.addComponent(parallaxXSpinner)
							.addComponent(parallaxYSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(depthLabel)
							.addComponent(depthSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(colorLabel)
							.addComponent(colorChooser)
							.addComponent(alphaLabel)
							.addComponent(alphaSpinner))
					.addComponent(separatorB, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(framesLabel)
							.addComponent(framesSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(frameTimeLabel)
							.addComponent(frameTimeSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(timeOffsetLabel)
							.addComponent(timeOffsetSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(frameOffsetLabel)
							.addComponent(frameOffsetXSpinner)
							.addComponent(frameOffsetYSpinner))
					.addComponent(randomizeTimeOffsetCheckbox)
					.addComponent(alignTimeOffsetWithFrameTimeCheckbox));

			// a hack to account for border sizes
			Insets insets = getInsets();
			setMinimumSize(new Dimension(320 + insets.left + insets.right, getMinimumSize().height));
			setSize(320 + insets.left + insets.right, 768 + insets.top + insets.bottom);

			// add listeners
			ChangeListener gridSettingsListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}

					final boolean showGrid = showGridCheckbox.isSelected();
					final int gridWidth = (Integer) gridWidthSpinner.getValue();
					final int gridHeight = (Integer) gridHeightSpinner.getValue();
					final int gridOffsetX = (Integer) gridXSpinner.getValue();
					final int gridOffsetY = (Integer) gridYSpinner.getValue();

					final boolean showImageGrid = showImageGridCheckbox.isSelected();
					final int imageGridWidth = (Integer) imageGridWidthSpinner.getValue();
					final int imageGridHeight = (Integer) imageGridHeightSpinner.getValue();
					final int imageGridOffsetX = (Integer) imageGridXSpinner.getValue();
					final int imageGridOffsetY = (Integer) imageGridYSpinner.getValue();

					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							TileEditor.this.showGrid = showGrid;
							TileEditor.this.gridWidth = gridWidth;
							TileEditor.this.gridHeight = gridHeight;
							TileEditor.this.gridOffsetX = gridOffsetX;
							TileEditor.this.gridOffsetY = gridOffsetY;

							TileEditor.this.showImageGrid = showImageGrid;
							TileEditor.this.imageGridWidth = imageGridWidth;
							TileEditor.this.imageGridHeight = imageGridHeight;
							TileEditor.this.imageGridOffsetX = imageGridOffsetX;
							TileEditor.this.imageGridOffsetY = imageGridOffsetY;

							finishedCommand();
						}
					});
				}
			};

			showGridCheckbox.addChangeListener(gridSettingsListener);
			gridWidthSpinner.addChangeListener(gridSettingsListener);
			gridHeightSpinner.addChangeListener(gridSettingsListener);
			gridXSpinner.addChangeListener(gridSettingsListener);
			gridYSpinner.addChangeListener(gridSettingsListener);

			showImageGridCheckbox.addChangeListener(gridSettingsListener);
			imageGridWidthSpinner.addChangeListener(gridSettingsListener);
			imageGridHeightSpinner.addChangeListener(gridSettingsListener);
			imageGridXSpinner.addChangeListener(gridSettingsListener);
			imageGridYSpinner.addChangeListener(gridSettingsListener);

			hideUnselectedLayersCheckbox.addChangeListener(new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}

					final boolean sel = hideUnselectedLayersCheckbox.isSelected();

					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							hideUnselectedLayers = sel;
							updateVisibility();
							finishedCommand();
						}
					});
				}
			});

			addButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// create the tile layer
							final RoomDefinition.TileLayerDefinition def =
									new RoomDefinition.TileLayerDefinition();
							final TileLayerInstance instance =
									new TileLayerInstance(def.getTileLayer(), null, getEngine().window.renderer);
							roomDefinition.addTileLayer(roomDefinition.getTileLayerCount(), def);
							((RoomState) getGameState()).addTileLayer(instance);
							tileLayersVisible.put(instance, true);
							updateVisibility();

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									// add to the list
									Node node = new Node();
									node.text = "Layer";
									node.layer = instance;

									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										model.addElement(node);
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
									list.setSelectedIndex(model.getSize() - 1);
								}
							});
						}
					});
				}
			});

			removeButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}
					if (listSelection < 0) {
						return;
					}
					// first enqueues the "deselect" command
					// current selection is stored, then removed
					final int oldSelection = listSelection;
					list.clearSelection();

					final Node node = model.get(listSelection);
					final TileLayerInstance instance = node.layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// remove the tile layer
							((RoomState) getGameState()).removeTileLayer(instance);
							// remove from definition
							for (int i = 0; i < roomDefinition.getTileLayerCount(); ++i) {
								if (roomDefinition.getTileLayer(i).getTileLayer() == instance.getTileLayer()) {
									roomDefinition.removeTileLayer(i);
									break;
								}
							}
							instance.release();
							activeTileLayer = null;
							tileLayersVisible.remove(instance);

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									model.remove(oldSelection);
								}
							});
						}
					});
				}
			});

			list.addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(ListSelectionEvent e) {
					if (!performCallbacks) {
						return;
					}
					if (commandsPending > 0) {
						// revert to previous selection, as if nothing happened
						boolean tempPerformCallbacks = performCallbacks;
						performCallbacks = false;
						try {
							list.setSelectedIndex(listSelection);
						} finally {
							performCallbacks = tempPerformCallbacks;
						}
						return;
					}

					final int depth = (Integer) depthSpinner.getValue();
					final float parallaxX = (Float) parallaxXSpinner.getValue();
					final float parallaxY = (Float) parallaxYSpinner.getValue();
					final float alpha = (Float) alphaSpinner.getValue();

					final TileLayerInstance oldInstance = (listSelection < 0) ?
							null : model.get(listSelection).layer;
					final TileLayerInstance newInstance = (list.getSelectedIndex() < 0) ?
							null : model.get(list.getSelectedIndex()).layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							if (oldInstance != null) {
								oldInstance.getTileLayer().setDepth(depth);
								oldInstance.getTileLayer().setParallax(parallaxX, parallaxY);
								Vector4f color = oldInstance.getTileLayer().getColor();
								color.w(alpha);
								oldInstance.getTileLayer().setColor(color);
							}

							// retrieve the new values
							final int depth = (newInstance == null) ? 0 : newInstance.getTileLayer().getDepth();
							final float parallaxX = (newInstance == null) ? 0.0f : newInstance.getTileLayer().getParallaxX();
							final float parallaxY = (newInstance == null) ? 0.0f : newInstance.getTileLayer().getParallaxY();
							final Vector4f color = (newInstance == null) ? new Vector4f() : newInstance.getTileLayer().getColor();

							activeTileLayer = newInstance;
							if (newInstance != oldInstance) {
								tilesetDragX = 0;
								tilesetDragY = 0;
							}

							updateVisibility();

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									listSelection = list.getSelectedIndex();
									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										depthSpinner.setValue(depth);
										parallaxXSpinner.setValue(parallaxX);
										parallaxYSpinner.setValue(parallaxY);
										colorChooser.setBackground(new Color(color.x(), color.y(), color.z()));
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
								}
							});
						}
					});
				}
			});

			setTextureButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}

					final String result = (String) JOptionPane.showInputDialog(TileEditorFrame.this,
							"Choose texture:", "Choose Texture", JOptionPane.PLAIN_MESSAGE, null,
							textureNames, null);
					if (result == null) {
						return;
					}

					final TileLayerInstance instance = model.get(listSelection).layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							GpuTexture texture = ((RoomState) getGameState()).getCoreState().resourceManager
									.getTexture(
											result);
							if (texture != null) {
								instance.setTexture(texture);
								// set definition texture
								for (int i = 0; i < roomDefinition.getTileLayerCount(); ++i) {
									if (roomDefinition.getTileLayer(i).getTileLayer() == instance.getTileLayer()) {
										roomDefinition.getTileLayer(i).setTexture(result);
										break;
									}
								}
							}

							finishedCommand();
						}
					});
				}
			});

			ChangeListener changeListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}

					final int depth = (Integer) depthSpinner.getValue();
					final float parallaxX = (Float) parallaxXSpinner.getValue();
					final float parallaxY = (Float) parallaxYSpinner.getValue();
					final float alpha = (Float) alphaSpinner.getValue();
					final TileLayerInstance instance = model.get(listSelection).layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							instance.getTileLayer().setDepth(depth);
							instance.getTileLayer().setParallax(parallaxX, parallaxY);
							Vector4f color = instance.getTileLayer().getColor();
							color.w(alpha);
							instance.getTileLayer().setColor(color);
							finishedCommand();
						}
					});
				}
			};

			depthSpinner.addChangeListener(changeListener);
			parallaxXSpinner.addChangeListener(changeListener);
			parallaxYSpinner.addChangeListener(changeListener);
			alphaSpinner.addChangeListener(changeListener);
			colorChooser.addMouseListener(new MouseListener() {
				@Override
				public void mouseClicked(MouseEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}
					if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 1) {
						final Color result = JColorChooser.showDialog(TileEditorFrame.this, "Choose Color",
								Color.WHITE);
						if (result == null) {
							return;
						}
						colorChooser.setBackground(result);

						final TileLayerInstance instance = model.get(listSelection).layer;
						enqueueCommand(new Runnable() {
							@Override
							public void run() {
								Vector4f color = instance.getTileLayer().getColor();
								color.x((float) result.getRed() / 255.0f);
								color.y((float) result.getGreen() / 255.0f);
								color.z((float) result.getBlue() / 255.0f);
								instance.getTileLayer().setColor(color);
								finishedCommand();
							}
						});
					}
				}

				public void mousePressed(MouseEvent e) {
				}

				public void mouseReleased(MouseEvent e) {
				}

				public void mouseEntered(MouseEvent e) {
				}

				public void mouseExited(MouseEvent e) {
				}
			});

			ChangeListener animationChangeListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}

					final int frames = (Integer) framesSpinner.getValue();
					final int frameTime = (Integer) frameTimeSpinner.getValue();
					final int timeOffset = (Integer) timeOffsetSpinner.getValue();
					final int frameOffsetX = (Integer) frameOffsetXSpinner.getValue();
					final int frameOffsetY = (Integer) frameOffsetYSpinner.getValue();
					final boolean randomizeTimeOffset = randomizeTimeOffsetCheckbox.isSelected();
					final boolean alignTimeOffsetWithFrameTime = alignTimeOffsetWithFrameTimeCheckbox.isSelected();
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							tileFrames = frames;
							tileFrameTime = frameTime;
							tileTimeOffset = timeOffset;
							tileFrameOffsetX = frameOffsetX;
							tileFrameOffsetY = frameOffsetY;
							randomizeTileTimeOffset = randomizeTimeOffset;
							alignTileTimeOffsetWithFrameTime = alignTimeOffsetWithFrameTime;
							finishedCommand();
						}
					});
				}
			};

			framesSpinner.addChangeListener(animationChangeListener);
			frameTimeSpinner.addChangeListener(animationChangeListener);
			timeOffsetSpinner.addChangeListener(animationChangeListener);
			frameOffsetXSpinner.addChangeListener(animationChangeListener);
			frameOffsetYSpinner.addChangeListener(animationChangeListener);
			randomizeTimeOffsetCheckbox.addChangeListener(animationChangeListener);
			alignTimeOffsetWithFrameTimeCheckbox.addChangeListener(animationChangeListener);

			listSelection = -1;

			performCallbacks = true;

			setVisible(true);
		}

		private void init() {
			RoomState roomState = (RoomState) getGameState();
			// make a copy to avoid concurrent modification errors
			final Set<TileLayerInstance> tileLayersF = new HashSet<TileLayerInstance>();
			tileLayersF.addAll(roomState.getTileLayers());

			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					performCallbacks = false;
					try {
						for (TileLayerInstance layer : tileLayersF) {
							Node node = new Node();
							node.layer = layer;
							node.text = "Layer";
							model.addElement(node);
						}
					} finally {
						performCallbacks = true;
					}
				}
			});
		}

		private int listSelection;

		private int commandsPending = 0;
		private void enqueueCommand(Runnable command) {
			commandQueue.add(command);
			++commandsPending;
		}
		private void finishedCommand() {
			finishedCommand(null);
		}
		private void finishedCommand(final Runnable uiCommand) {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					--commandsPending;
					if (uiCommand != null) {
						uiCommand.run();
					}
				}
			});
		}
	}
}
